/*

Copyright 2009 Nicola Dal Pont

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

package com.astoolkit.workflow
{
    import com.astoolkit.workflow.constant.CLOSED_OUTLET;
    import com.astoolkit.workflow.constant.EMPTY_PIPELINE;
    import com.astoolkit.workflow.constant.EmptyPipelinePolicy;
    import com.astoolkit.workflow.constant.FailurePolicy;
    import com.astoolkit.workflow.constant.Feed;
    import com.astoolkit.workflow.constant.Flow;
    import com.astoolkit.workflow.constant.PIPELINE_OUTLET;
    import com.astoolkit.workflow.constant.TaskStatus;
    import com.astoolkit.workflow.constant.UNDEFINED;
    import com.astoolkit.workflow.util.DataWrapper;
    
    import flash.events.EventDispatcher;
    import flash.events.IEventDispatcher;
    import flash.net.registerClassAlias;
    import flash.utils.getQualifiedClassName;
    import flash.utils.setTimeout;
    
    import mx.core.ClassFactory;
    import mx.core.IFactory;
    import mx.events.PropertyChangeEvent;
    import mx.events.PropertyChangeEventKind;
    import mx.logging.ILogger;
    import mx.logging.Log;
    import mx.rpc.Fault;
    import mx.utils.ObjectUtil;
	
	[Event(
		name="subtaskInitialized",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="subtaskPrepared",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="subtaskStarted",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="subtaskDataSet",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="subtaskWarning",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="subtaskFault",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="subtaskCompleted",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="subtaskProgress",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="subtaskAborted",
		type="com.astoolkit.workflow.WorkflowEvent")]
	
    [DefaultProperty("tasks")]
    public class Workflow 
    	extends BasePipelineTask 
    	implements IWorkflow
    {
		protected static const LOGGER : ILogger =
			Log.getLogger( getQualifiedClassName( Workflow ).replace(/:+/g, "." ) );
		

		protected var _root : IWorkflow;
		
		protected var _insert : Array;
		
		protected var _childrenDelegate : IWorkflowDelegate;
		
		protected var _iterate : Boolean = false;
		
		protected var _retain : Boolean = false;
		
        protected var _flow : String = Flow.SERIAL;
       
		protected var _feed : String = Feed.PIPELINE;
		
		private static var _retainedWorkflows : Object = [];
       
		protected var _subPipelineData : *;
		
        [ArrayElementType("com.astoolkit.workflow.IWorkflowTask")]
        protected var _processedTasks : Array = []
       
        protected var _nextTaskIndex : int = 0;
       
        [ArrayElementType("com.astoolkit.workflow.IWorkflowTask")]
        protected var _tasks : Array = [];

		private var _contextFactory : IFactory;
		
		
		[Bindable]
        public var properties : Array = [];
		
        public function Workflow() {
            super();
			_childrenDelegate = createDelegate();
        }

		override public function get async() : Boolean
		{
			return true;
		}
		
		public function get contextFactory() : IFactory
		{
			return _contextFactory;
		}
		
		public function set contextFactory( inFactory : IFactory ) : void
		{
			if( inFactory )
				_contextFactory = inFactory;
		}

		public function get iterate() : Boolean
		{
			return _iterate;
		}
		
		public function set iterate( inIterate: Boolean ) : void
		{
			_iterate = inIterate;
		}		
		
		public function set insert( inValue : Array ) : void
		{
			if( _insert )
				throw new Error( "insert cannot be redefined" );
			_insert = inValue;
		}

		
       
		public function get flow() : String
		{
			return _flow;
		}
		
		[Inspectable(defaultValue="serial", enumeration="parallel,serial,none")]
		public function set flow( inFlow : String ) : void
		{
			if( inFlow != Flow.SERIAL && inFlow != Flow.PARALLEL && inFlow != Flow.NONE )
			{
				throw new Error(
					"Workflow 'flow' property can only be either 'parallel' or 'serial' or 'none': " +
					"found '" + inFlow + "'" );
			}
			_flow = inFlow;
		}
		
		public function get feed() : String
		{
			return _feed;
		}
		
		[Inspectable(defaultValue="pipeline", enumeration="pipeline")]
		public function set feed( inFeed : String ) : void
		{
			if( inFeed != Feed.PIPELINE )
			{
				throw new Error(
					"Workflow 'feed' property can only be 'pipeline': " +
					"found '" + inFeed + "'" );
			}
			_feed = inFeed;
		}
		
		[Bindable]
        [ArrayElementType("com.astoolkit.workflow.IWorkflowTask")]
        public function get tasks() : Array
        {
            return _tasks;   
        }
        
        public function set tasks( inTasks : Array ) : void
        {
			if( _tasks != null && _tasks.length > 0 )
				throw new Error( "Tasks list cannot be overridden" );
            _tasks = inTasks;
        }


		public function get declaredTasks() : Array
		{
			return _tasks;
		}
		
		protected function createDelegate() : IWorkflowDelegate
		{
			var delegate : IWorkflowDelegate = new DefaultWorkflowDelegate();
			delegate.onAbort = onSubtaskAbort;
			delegate.onInitialize = onSubtaskInitialized;
			delegate.onPrepare = onSubtaskPrepared;
			delegate.onBegin = onSubtaskBegin;
			delegate.onComplete = onSubtaskCompleted;
			delegate.onFault = onSubtaskFault
			delegate.onProgress = onSubtaskProgress;
			delegate.onResume = onSubtaskResumed;
			delegate.onSuspend = onSubtaskSuspended;
			delegate.onWarning = onSubtaskWarning;
			return delegate;
		}
		       
        override public function begin( inRetain : Boolean = false) : void
        {
			_subPipelineData = UNDEFINED;
			if(!_delegate)
				_delegate = createDelegate();
			if( !_parent )
				initialize();
			super.begin();
			if( !_parent )
				prepare();
            if( _tasks == null || _tasks.length == 0 )
            {
                LOGGER.warn( "Workflow {0} has no tasks to perform", _description );
				complete();
				return;
            }
			for each( var task : IWorkflowTask in tasks )
			{
				task.prepare();
			}

			if( inRetain && !_retainedWorkflows.hasOwnProperty(_root) )
				_retainedWorkflows[_root] = _root;
			
			_status = TaskStatus.RUNNING;
            runNextTask();
        }
       
       protected function runNextTask() : void
        {
            if( _nextTaskIndex == _tasks.length )
			{
				if( flow == Flow.SERIAL ) 
					complete();
                return;
            }
            var task : IWorkflowTask = _tasks[ _nextTaskIndex ];
			setSubtaskPipelineData( task );
			_document.$self = new DataWrapper(task);
			dispatchTaskEvent( WorkflowEvent.DATA_SET, task );
			_nextTaskIndex++;
			if( task is IWorkflow && IWorkflow( task ).flow == Flow.NONE )
			{
				runNextTask();
				return;
			}
            //LOGGER.debug(
            //    "Next task '{0}' ({1})", task.title, getQualifiedClassName( task ) );
            if( task.enabled && ( !(task is IPipelineTask) || ( IPipelineTask( task ).emptyPipelinePolicy != EmptyPipelinePolicy.SKIP || _subPipelineData != EMPTY_PIPELINE ) ) )
            {
                _processedTasks.push( task );
				runSubTask( task );
            } else {
                runNextTask();
                return;
            }
            if( _flow == Flow.PARALLEL )
            {
                runNextTask();
            }
        }

	   /**
		 * @private
		 */
		protected function onSubtaskSuspended( inTask : IWorkflowTask ) : void
		{
			if( root.context.status != TaskStatus.SUSPENDED )
			{
				context.status = TaskStatus.SUSPENDED;
				context.dispatchEvent( new WorkflowEvent( WorkflowEvent.SUSPENDED ) );
			}
			dispatchTaskEvent( WorkflowEvent.SUSPENDED, inTask );
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskResumed( inTask : IWorkflowTask ) : void
		{
			if( context.status == TaskStatus.SUSPENDED )
			{
				context.status = TaskStatus.RUNNING;
				context.dispatchEvent( new WorkflowEvent( WorkflowEvent.RESUMED ) );
			}
			dispatchTaskEvent( WorkflowEvent.RESUMED, inTask );
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskBegin( inTask : IWorkflowTask ) : void
		{
			dispatchTaskEvent( WorkflowEvent.STARTED, inTask );
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskProgress( inTask : IWorkflowTask ) : void
		{
			if( inTask.status == TaskStatus.SUSPENDED )
				return;
			dispatchTaskEvent( WorkflowEvent.PROGRESS, inTask );
		}
		

		/**
		 * @private
		 */
		protected function onSubtaskWarning( inTask : IWorkflowTask, inMessage : String, inError : Fault ) : void
        {
			dispatchTaskEvent( WorkflowEvent.WARNING, inTask, inMessage, inError);
			onSubtaskCompleted( inTask );
        }
       
		/**
		 * @private
		 */
		protected function onSubtaskFault( inTask : IWorkflowTask, inMessage : String, inFault : Fault ) : void
        {
			if( inTask.status != TaskStatus.ABORTED )
			{
				_context.failedTask = inTask;
	            if( inTask.failurePolicy == FailurePolicy.ABORT )
	            {
					LOGGER.error( 
						"Task {0} ({1}) failed. Cause:\n{2}\n{3}", 
						inTask.description, getQualifiedClassName( inTask ),
						inMessage,
						(inFault ? inFault.getStackTrace() : "") 
					);
					dispatchTaskEvent( WorkflowEvent.FAULT, inTask, inMessage, inFault );
	                if( _parent == null )
	                {
	                    cleanUp();                   
	                }
					abort();
	            }
	            else if( inTask.failurePolicy == FailurePolicy.SUSPEND )
				{
					inTask.suspend();
	            	onSubtaskCompleted( inTask );
	            }
				else if( inTask.failurePolicy == FailurePolicy.CONTINUE )
				{
					complete();
				}
				else
					onSubtaskCompleted( inTask );
			}
        }

		/**
		 * @private
		 */
		protected function onSubtaskPrepared( inTask : IWorkflowTask ) : void
		{
			dispatchTaskEvent( WorkflowEvent.PREPARED, inTask );
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskInitialized( inTask : IWorkflowTask ) : void
		{
			dispatchTaskEvent( WorkflowEvent.INITIALIZED, inTask );
		}
		
		/**
		 * @private
		 */
		protected function onSubtaskAbort( inTask : IWorkflowTask, inMessage : String ) : void
		{
			dispatchTaskEvent( WorkflowEvent.ABORTED, inTask, inMessage );
			onSubtaskCompleted( inTask );
		}
	
		/**
		 * @private
		 */
		protected function onSubtaskCompleted( inTask : IWorkflowTask ) : void
        {
			if( inTask == this )
			{
				dispatchTaskEvent( WorkflowEvent.COMPLETED, inTask );
				return;
			}
			if( inTask.status != TaskStatus.ABORTED )
			{
				dispatchTaskEvent( WorkflowEvent.COMPLETED, inTask );
	            _processedTasks.splice( _processedTasks.indexOf( inTask ), 1 );
				//inTask.delegate = null;
				if( !( inTask is IPipelineTask ) || !IPipelineTask( inTask ).ignoreOutput )
				{
					if( inTask is IPipelineTask && context.failedTask != inTask )
					{
						if( IPipelineTask(inTask).outlet == PIPELINE_OUTLET ) 
							_subPipelineData = IPipelineTask( inTask ).taskOutput;
						else if( IPipelineTask(inTask).outlet is String && ( IPipelineTask(inTask).outlet as String ).match( /^\$\w+$/ ) )
						{
							context.variables[ ( IPipelineTask( inTask ).outlet as String).substr(1) ] = IPipelineTask( inTask ).taskOutput;
							IEventDispatcher( _document ).dispatchEvent(
								new PropertyChangeEvent(
									PropertyChangeEvent.PROPERTY_CHANGE,
									false,
									false,
									PropertyChangeEventKind.UPDATE,
									"$",
									Math.random(),
									context.variables
									) );
									
						}
						else if( IPipelineTask(inTask).outlet != CLOSED_OUTLET )
						{
							_subPipelineData = IPipelineTask( inTask ).outlet;
						}
					}
				}
			}
			context.failedTask = null;
            if( _flow == Flow.SERIAL && _nextTaskIndex < _tasks.length )
            {
                runNextTask();
                return;
            }
            else
            {
                if( _processedTasks.length == 0 )
                {
                    complete();
                }
            }
        }
       
		protected function runSubTask( inTask : IWorkflowTask, inNow : Boolean = false ) : void
		{
			if( _status == TaskStatus.SUSPENDED ) {
				addResumeCallBack( 
					function() : void 
					{
						runSubTask( inTask, inNow );
					} );
				return;
			}
			if( inNow || inTask.delay == 0 )
			{
				try {
					if( inTask is IWorkflow && IWorkflow( inTask ).contextFactory == null )
						IWorkflow( inTask ).contextFactory = _contextFactory;
					inTask.begin();
				}
				catch( taskError : Error )
				{
					_childrenDelegate.onFault(inTask, taskError.message, new Fault( taskError.errorID.toString(), taskError.message, taskError.getStackTrace() ) );
					LOGGER.error( taskError.getStackTrace() );
				}
			}
			else
			{
				setTimeout( runSubTask, inTask.delay, inTask, true );
			}
		}
		
		protected function setSubtaskPipelineData( inTask : IWorkflowTask ) : void
		{
			if( _subPipelineData == UNDEFINED )
			{
				_subPipelineData = pipelineData;
			}
			if( inTask is IPipelineTask && IPipelineTask( inTask ).taskOutput == UNDEFINED )
			{
				if( _document )
					_document.$data = new DataWrapper(_subPipelineData);
				if( IPipelineTask( inTask ).inlet != null )
				{
					try
					{
						if( Object( inTask ).hasOwnProperty( IPipelineTask( inTask ).inlet ) )
						{
							if( inTask[ IPipelineTask( inTask ).inlet ] is Function )
							{
								var f : Function = inTask[ IPipelineTask( inTask ).inlet ] as Function; 
								f.apply( inTask, [ _subPipelineData ] );
							}
							else
								inTask[ IPipelineTask( inTask ).inlet ] = _subPipelineData;
						}
					}
					catch( e : Error )
					{
						trace( e.getStackTrace() );
						//TODO: failure management;
					}
				}
				IPipelineTask( inTask ).taskInput = _subPipelineData;
			}
		}
		
        override public function cleanUp() : void
        {
			super.cleanUp();
            for each( var task : IWorkflowTask in declaredTasks )
            {
				//task.delegate = null;
				task.cleanUp();
            }
            _processedTasks = [];
            _nextTaskIndex = 0;
			if( this == _root && _retainedWorkflows.hasOwnProperty(_root) )
			{
				delete _retainedWorkflows.hasOwnProperty[_root];
			}
        }

		override public function initialize() : void
		{
			if( _status != TaskStatus.STOPPED )
				return;
			super.initialize();
			
			if( !_contextFactory )
				_contextFactory = new ClassFactory( DefaultContext );
			if( !_context )
				_context = _contextFactory.newInstance() as IWorkflowContext;
			_context.failedTask = null;
			if( declaredTasks.length > 0 )
			{
				for each( var task : IWorkflowTask in declaredTasks )
				{						
					task.delegate = _childrenDelegate;
					task.context = _context;
					task.parent = this;
					task.initialize();
				}
			}
			processInserts();
		}

		override public function prepare() : void
		{
			super.prepare();
			_processedTasks = [];
			_nextTaskIndex = 0;
			
			var task : IWorkflowTask;
			for each( task in declaredTasks )
			{
				task.delegate = _childrenDelegate;
				task.prepare();
			}
				

		}

		override protected function complete() : void
		{
			if( !iterate )
			{
				super.complete();
				if( !_ignoreOutput )
					_pipelineData = _subPipelineData;
				if( !_parent )
					cleanUp();
			}
			else
			{
				prepare();
				setTimeout(runNextTask, 1);
			}
		}


		protected function processInserts() : void
		{
			var task : IWorkflowTask;
			if( _insert ) 
			{
				for each( var insertEntry : Insert in _insert )
				{
					if( insertEntry.relativeTo != null )
						insertEntry.parent = insertEntry.relativeTo.parent;
					
					var childrenTasks : Array = insertEntry.parent.declaredTasks;
					var insertionPoint : int;
					if( insertEntry.relativeTo != null )
					{
						var i : int;
						for( i = 0; i < childrenTasks.length; i++ )
						{
							if( insertEntry.relativeTo == childrenTasks[ i ] )
							{
								break;
							}
						}
						insertionPoint = insertEntry.position == Insert.BEFORE ? i : i + 1;
					}
					else
					{
						// if children length == 0 we always add the task starting at 0,
						// otherwise depending on the position value we add 
						// to the beginning or end of the tasks array
						insertionPoint = childrenTasks.length > 0 ?
							(insertEntry.position == Insert.BEFORE ? 0 : childrenTasks.length ) : 0;
					}
					
					for each( task in insertEntry.tasks )
					{
						childrenTasks.splice( insertionPoint, 0, task );
						insertionPoint++;
					}
					
				}
			}
			_insert = [];
			
		}
		
		protected function dispatchTaskEvent( 
			inEventType : String, 
			inTask : IWorkflowTask, 
			inMessage : String = null, 
			inFault : Fault = null ) : void
		{
			var task : IWorkflowTask = inTask;
			var subEventType : String = "subtask" + inEventType.substr(0,1).toUpperCase() + inEventType.substr(1);
			while( task )
			{
				if( inTask == task ) {
					if( task.hasEventListener( inEventType ) )
						task.dispatchEvent( new WorkflowEvent( inEventType, inTask, inMessage, inFault ) );
				}
				else
				{
					if( task.hasEventListener( subEventType ) )
						task.dispatchEvent( new WorkflowEvent( subEventType, inTask, inMessage, inFault ) );
				}
				task = task.parent;
			}
		}

	}
}
